package com.scd.jmeter.rmi.client;

import com.scd.jmeter.rmi.common.JMeterEngine;
import com.scd.jmeter.rmi.common.TestPlan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

public class DistributedRunner {
    private static final Logger log = LoggerFactory.getLogger(DistributedRunner.class);

    private static final String HOST_NOT_FOUND_MESSAGE = "Host not found in list of active engines: {}";

    private final Map<String, JMeterEngine> engines = new HashMap<>();

    public static final String RETRIES_NUMBER = "client.tries"; // $NON-NLS-1$
    public static final String RETRIES_DELAY = "client.retries_delay"; // $NON-NLS-1$
    public static final String CONTINUE_ON_FAIL = "client.continue_on_fail"; // $NON-NLS-1$

    private final boolean continueOnFail;

    private final int retriesDelay;

    private final int retriesNumber;

    public DistributedRunner(Properties props) {
        retriesNumber = (int) props.getOrDefault(RETRIES_NUMBER, 1);
        continueOnFail = (boolean) props.getOrDefault(CONTINUE_ON_FAIL, false);
        retriesDelay = (int) props.getOrDefault(RETRIES_DELAY, 5000);
    }

    public void init(List<String> addresses, TestPlan testPlan) {
        // converting list into mutable version
        List<String> addrs = new ArrayList<>(addresses);

        for (int tryNo = 0; tryNo < retriesNumber; tryNo++) {
            if (tryNo > 0) {
                log.info("Following remote engines will retry configuring: " + addrs+", pausing before retry for " + retriesDelay + "ms");
                try {
                    Thread.sleep(retriesDelay);
                } catch (InterruptedException e) {  // NOSONAR
                    throw new IllegalStateException("Interrupted while initializing remote engines:"+addrs, e);
                }
            }

            int idx = 0;
            while (idx < addrs.size()) {
                String address = addrs.get(idx);
                log.info("Configuring remote engine: " + address);
                JMeterEngine engine = getClientEngine(address.trim(), testPlan);
                if (engine != null) {
                    engines.put(address, engine);
                    addrs.remove(address);
                } else {
                    log.info("Failed to configure " + address);
                    idx++;
                }
            }

            if (addrs.isEmpty()) {
                break;
            }
        }

        if (!addrs.isEmpty()) {
            String msg = "Following remote engines could not be configured:" + addrs;
            log.info("init msg {}", msg);
            if (!continueOnFail || engines.isEmpty()) {
                stop();
                throw new RuntimeException(msg); // NOSONAR
            } else {
                log.info("Continuing without failed engines...");
            }
        }
    }

//    private static String formatLikeDate(Instant instant) {
//        return DateTimeFormatter
//                .ofLocalizedDateTime(FormatStyle.LONG)
//                .withLocale(Locale.ROOT)
//                .withZone(ZoneId.systemDefault())
//                .format(instant);
//    }

    /**
     * Starts a remote testing engines
     *
     * @param addresses list of the DNS names or IP addresses of the remote testing engines
     */
    public void start(List<String> addresses) {
//        Instant now = Instant.now();
//        println("Starting distributed test with remote engines: "
//                + addresses + " @ " + formatLikeDate(now) + " (" + now.toEpochMilli() + ')');
        List<String> startedEngines = new ArrayList<>(addresses.size());
        List<String> failedEngines = new ArrayList<>(addresses.size());
        for (String address : addresses) {
            JMeterEngine engine = engines.get(address);
            try {
                if (engine != null) {
                    engine.runTest();
                    startedEngines.add(address);
                } else {
                    failedEngines.add(address);
                }
            } catch (IllegalStateException e) { // NOSONAR already reported to user
                failedEngines.add(address);
            }
        }
        if (!failedEngines.isEmpty()) {
            log.error("The following remote engines have not started:{}", failedEngines);
        }
    }

    /**
     * Start all engines that were previously initiated
     */
    public void start() {
        List<String> addresses = new ArrayList<>(engines.keySet());
        start(addresses);
    }

    public void stop(List<String> addresses) {
        log.info("Stopping remote engines");
        for (String address : addresses) {
            try {
                JMeterEngine engine = engines.get(address);
                if (engine != null) {
                    engines.get(address).stopTest(true);
                } else {
                    log.info(HOST_NOT_FOUND_MESSAGE, address);
                }
            } catch (RuntimeException e) {
                log.error("Failed to stop test on " + address, e);
            }
        }
        log.info("Remote engines have been stopped");
    }

    /**
     * Stop all engines that were previously initiated
     */
    public void stop() {
        List<String> addresses = new ArrayList<>(engines.keySet());
        stop(addresses);
    }

    public void shutdown(List<String> addresses) {
        log.info("Shutting down remote engines");
        for (String address : addresses) {
            try {
                if (engines.containsKey(address)) {
                    engines.get(address).stopTest(false);
                } else {
                    log.warn(HOST_NOT_FOUND_MESSAGE, address);
                }

            } catch (RuntimeException e) {
                log.error("Failed to shutdown test on " + address, e);
            }
        }
        log.info("Remote engines have been shut down");
    }

    public void exit(List<String> addresses) {
        log.info("Exiting remote engines");
        for (String address : addresses) {
            try {
                if (engines.containsKey(address)) {
                    engines.get(address).exit();
                } else {
                    log.warn(HOST_NOT_FOUND_MESSAGE, address);
                }
            } catch (RuntimeException e) {
                log.error("Failed to exit on " + address, e);
            }
        }
        log.info("Remote engines have been exited");
    }

    private JMeterEngine getClientEngine(String address, TestPlan testPlan) {
        JMeterEngine engine;
        try {
            engine = createEngine(address);
            engine.configure(testPlan);
        } catch (Exception ex) {
            log.error("Failed to create engine at {}", address, ex);
            return null;
        }
        return engine;
    }


    /**
     * A factory method that might be overridden for unit testing
     *
     * @param address address for engine
     * @return engine instance
     * @throws RemoteException if registry can't be contacted
     * @throws NotBoundException when name for address can't be found
     */
    protected JMeterEngine createEngine(String address) throws RemoteException, NotBoundException {
        return new ClientJMeterEngine(address);
    }
}
